package com.clei.utils;

import com.clei.dto.file.VirFile;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

/**
 * zip util
 *
 * @author Y
 * @date 2023-12-25
 **/
public class ZipUtil {

    /**
     * buffer size
     */
    private static final int BUFFER_SIZE = 1024;

    private static final Charset DEFAULT_CHARSET = SystemUtil.isWindows()
            ? Charset.forName("GBK") : StandardCharsets.UTF_8;

    /**
     * 将一个文件或文件夹压缩为zip文件
     *
     * @param filePath 源文件路径
     * @param outPath  输出文件目录
     */
    public static void zip(String filePath, String outPath) {
        zip(filePath, outPath, null, true);
    }

    /**
     * 将一个文件或文件夹压缩为zip文件
     *
     * @param filePath    源文件路径
     * @param outPath     输出文件目录
     * @param includeSelf 源文件为目录时是否包含自身
     */
    public static void zip(String filePath, String outPath, boolean includeSelf) {
        zip(filePath, outPath, null, includeSelf);
    }

    /**
     * 将一个文件或文件夹压缩为zip文件
     *
     * @param filePath 源文件路径
     * @param outPath  输出文件目录
     */
    public static void zip(String filePath, String outPath, String zipFileName) {
        zip(filePath, outPath, zipFileName, true);
    }

    /**
     * 将一个文件或文件夹压缩为zip文件
     *
     * @param filePath    源文件路径
     * @param outPath     输出文件目录
     * @param includeSelf 源文件为目录时是否包含自身
     */
    public static void zip(String filePath, String outPath, String zipFileName, boolean includeSelf) {
        File sourceFile = new File(filePath);
        String fileName = sourceFile.getName();
        if (null == zipFileName) {
            zipFileName = fileName + ".zip";
        }
        File targetFile = new File(outPath + File.separator + zipFileName);
        if (!targetFile.getParentFile().exists()) {
            targetFile.getParentFile().mkdirs();
        }
        try (FileOutputStream fos = new FileOutputStream(targetFile);
             ZipOutputStream zos = new ZipOutputStream(fos)) {
            compress(sourceFile, zos, fileName, !includeSelf);
        } catch (Exception e) {
            PrintUtil.log("文件压缩出错", e);
        }
    }

    /**
     * zip
     *
     * @param virFile      文件
     * @param outputStream OutputStream
     */
    public static void zip(VirFile virFile, OutputStream outputStream) {
        try (ZipOutputStream zos = new ZipOutputStream(outputStream)) {
            compress(virFile, virFile.getFileName(), zos);
        } catch (Exception e) {
            PrintUtil.log("文件压缩出错", e);
        }
    }

    /**
     * 提取某个部分为zip
     *
     * @param zipPath     zip文件路径
     * @param partName    提取文件名称
     * @param outPath     输出路径
     * @param zipFileName 提取压缩文件名称
     * @param isDirectory 提取部分是否为目录
     * @param includeSelf 提取部分为目录时是否包含自身
     */
    public static void zipExtractToZip(String zipPath, String partName, String outPath, String zipFileName,
                                       boolean isDirectory, boolean includeSelf) {
        File writeFile = new File(outPath + File.separator + zipFileName);
        writeFile.getParentFile().mkdirs();
        try (ZipFile zipFile = new ZipFile(zipPath, DEFAULT_CHARSET);
             FileOutputStream fos = new FileOutputStream(writeFile);
             BufferedOutputStream bos = new BufferedOutputStream(fos);
             ZipOutputStream zos = new ZipOutputStream(bos)) {
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            // 前缀信息
            boolean findPart = false;
            String partPrefix = null;
            int subStart = 0;
            partName = isDirectory ? partName + "/" : partName;
            // 提取
            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                String entryName = entry.getName();
                if (!findPart) {
                    // 前缀、截取处理
                    if (entryName.endsWith(partName)) {
                        findPart = true;
                        if (isDirectory) {
                            partPrefix = entryName;
                            subStart = includeSelf ? entryName.length() - partName.length() : entryName.length();
                        }
                    } else if (entryName.startsWith(partName)) {
                        findPart = true;
                        if (isDirectory) {
                            partPrefix = partName;
                            subStart = includeSelf ? 0 : partName.length();
                        }
                    }
                    // 初始写入
                    if (findPart) {
                        if (isDirectory) {
                            if (includeSelf) {
                                putEntry(zos, partName, zipFile, entry);
                            }
                            if (entryName.startsWith(partName) && !entry.isDirectory()) {
                                putEntry(zos, entryName.substring(subStart), zipFile, entry);
                            }
                        } else {
                            putEntry(zos, partName, zipFile, entry);
                            // 提取部分为文件且已找到
                            break;
                        }
                    }
                } else {
                    if (entryName.startsWith(partPrefix)) {
                        putEntry(zos, entryName.substring(subStart), zipFile, entry);
                    }
                }
            }
        } catch (Exception e) {
            PrintUtil.log("zipExtractToZip error zipPath : {}, partName : {}", zipPath, partName, e);
        }
    }

    /**
     * 将一个zip文件解压到指定目录
     *
     * @param zipPath zip文件路径
     * @param outPath 输出文件目录
     */
    public static void unZip(String zipPath, String outPath) {
        try (ZipFile zipFile = new ZipFile(zipPath, DEFAULT_CHARSET)) {
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            byte[] buf = new byte[1024];
            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                String entryName = entry.getName();
                String fileName = outPath + "/" + entryName;
                if (entry.isDirectory()) {
                    File tempFile = new File(fileName);
                    tempFile.mkdirs();
                } else {
                    InputStream is = zipFile.getInputStream(entry);
                    FileOutputStream fos = new FileOutputStream(fileName);
                    int read;
                    while (-1 != (read = is.read(buf))) {
                        fos.write(buf, 0, read);
                    }
                    fos.flush();
                    fos.close();
                    is.close();
                }
            }
        } catch (Exception e) {
            PrintUtil.log("文件解压出错", e);
        }
    }

    /**
     * 判断一个文件是否是gzip文件
     *
     * @param gzPath 文件全路径
     * @return boolean
     */
    public static boolean isGzip(String gzPath) {
        try (FileInputStream fis = new FileInputStream(gzPath);
             BufferedInputStream bis = new BufferedInputStream(fis)) {
            byte[] buf = new byte[3];
            bis.read(buf);
            // gzip文件魔法数 1F8B08
            return (byte) (0x1F) == buf[0] && (byte) (0x8B) == buf[1] && (byte) (0x08) == buf[2];
        } catch (Exception e) {
            PrintUtil.log("文件解压出错", e);
            return false;
        }
    }

    /**
     * 将一个gz文件解压到指定目录
     *
     * @param gzPath  gz文件路径
     * @param outPath 输出文件目录
     */
    public static void unGzip(String gzPath, String outPath) {
        try (FileInputStream fis = new FileInputStream(gzPath);
             BufferedInputStream bis = new BufferedInputStream(fis);
             GzipCompressorInputStream gzi = new GzipCompressorInputStream(bis);
             FileOutputStream fos = new FileOutputStream(outPath + File.separator + gzi.getMetaData().getFilename())) {
            byte[] buf = new byte[1024];
            int len;
            while (-1 != (len = gzi.read(buf))) {
                fos.write(buf, 0, len);
            }
            fos.flush();
        } catch (Exception e) {
            PrintUtil.log("文件解压出错", e);
        }
    }

    /**
     * putEntry
     *
     * @param zos          ZipOutputStream
     * @param newEntryName newEntryName
     * @param oldEntry     oldEntry
     * @param oldFile      oldFile
     * @throws Exception Exception
     */
    private static void putEntry(ZipOutputStream zos, String newEntryName, ZipFile oldFile, ZipEntry oldEntry) throws Exception {
        zos.putNextEntry(new ZipEntry(newEntryName));
        // 写入文件数据
        if (!oldEntry.isDirectory()) {
            try (InputStream is = oldFile.getInputStream(oldEntry)) {
                StreamUtil.nioTransfer(is, zos, false);
            } catch (Exception e) {
                PrintUtil.log("write entry error entry : {}", newEntryName);
            }
        }
        zos.closeEntry();
    }

    /**
     * compressGz
     *
     * @param data data
     * @return byte[]
     */
    public static byte[] compressGz(byte[] data) {
        byte[] res = null;
        try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            compressGz(bais, baos);
            res = baos.toByteArray();
            baos.flush();
        } catch (Exception e) {
            PrintUtil.log("compressGz error data : {}", ByteUtil.toHex(data), e);
        }
        return res;
    }

    /**
     * deCompressGz
     *
     * @param data data
     * @return byte[]
     */
    public static byte[] deCompressGz(byte[] data) {
        byte[] res = null;
        try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            deCompressGz(bais, baos);
            res = baos.toByteArray();
            baos.flush();
        } catch (Exception e) {
            PrintUtil.log("compressGz error data : {}", ByteUtil.toHex(data), e);
        }
        return res;
    }

    /**
     * compressGz
     *
     * @param is InputStream
     * @param os OutputStream
     */
    private static void compressGz(InputStream is, OutputStream os) {
        try (GZIPOutputStream gos = new GZIPOutputStream(os)) {
            int length;
            byte[] data = new byte[BUFFER_SIZE];
            while ((length = is.read(data, 0, BUFFER_SIZE)) != -1) {
                gos.write(data, 0, length);
            }
            gos.finish();
            gos.flush();
        } catch (Exception e) {
            PrintUtil.log("compressGz error ", e);
        }
    }

    /**
     * deCompressGz
     *
     * @param is InputStream
     * @param os OutputStream
     */
    private static void deCompressGz(InputStream is, OutputStream os) {
        try (GZIPInputStream gis = new GZIPInputStream(is)) {
            int length;
            byte[] data = new byte[BUFFER_SIZE];
            while ((length = gis.read(data, 0, BUFFER_SIZE)) != -1) {
                os.write(data, 0, length);
            }
        } catch (Exception e) {
            PrintUtil.log("deCompressGz error ", e);
        }
    }

    /**
     * 递归压缩文件
     *
     * @param sourceFile     源文件
     * @param zos            zos
     * @param fileName       文件名
     * @param rootAndExclude 是根目录且排除
     * @throws Exception Exception
     */
    private static void compress(File sourceFile, ZipOutputStream zos, String fileName, boolean rootAndExclude) throws Exception {
        if (sourceFile.isDirectory()) {
            File[] files = sourceFile.listFiles();
            if (rootAndExclude) {
                if (null != files) {
                    for (File f : files) {
                        compress(f, zos, f.getName(), false);
                    }
                }
            } else {
                zos.putNextEntry(new ZipEntry(fileName + "/"));
                zos.closeEntry();
                if (null != files) {
                    for (File f : files) {
                        compress(f, zos, fileName + "/" + f.getName(), false);
                    }
                }
            }
        } else {
            zos.putNextEntry(new ZipEntry(fileName));
            FileInputStream fis = new FileInputStream(sourceFile);
            int read;
            byte[] buf = new byte[1024];
            while (-1 != (read = fis.read(buf))) {
                zos.write(buf, 0, read);
            }
            zos.closeEntry();
            fis.close();
        }
    }

    /**
     * 递归压缩文件
     *
     * @param virFile  文件
     * @param fileName 文件名
     * @param zos      ZipOutputStream
     * @throws Exception Exception
     */
    private static void compress(VirFile virFile, String fileName, ZipOutputStream zos) throws Exception {
        if (virFile.isDirectory()) {
            List<VirFile> children = virFile.getChildren();
            zos.putNextEntry(new ZipEntry(fileName + "/"));
            zos.closeEntry();
            for (VirFile child : children) {
                compress(child, fileName + "/" + child.getFileName(), zos);
            }
        } else {
            zos.putNextEntry(new ZipEntry(fileName));
            // 文件流
            InputStream inputStream = null;
            try {
                URL url = new URL(virFile.getFileUrl());
                URLConnection urlConnection = url.openConnection();
                urlConnection.setConnectTimeout(5 * 1000);
                inputStream = urlConnection.getInputStream();
                int read;
                byte[] buf = new byte[1024];
                while (-1 != (read = inputStream.read(buf))) {
                    zos.write(buf, 0, read);
                }
            } catch (Exception e) {
                PrintUtil.log("getHttpFileStream error fileUrl : {}", virFile.getFileUrl(), e);
            }
            zos.closeEntry();
            // streamClose
            if (null != inputStream) {
                try {
                    inputStream.close();
                } catch (Exception e) {
                    PrintUtil.log("stream close error fileUrl : {}", virFile.getFileUrl(), e);
                }
            }
        }
    }
}
